UserTalk has a with statement to help you work with names of deeply nested objects. For example, the following script works with the values in the webServer.preferences table:
with suites.webserver.preferences
« refer to suites.webserver.preferences.framework
if framework ≠ "3.0.1"
dialog.notify ("This script requires version 3.0.1")
return (false)
« ditto for server and verboseLogging
server = "WebSTAR 1.2 via Frontier"
verboseLogging = true
edit (@errorPage)
The CGI Framework
The CGI Framework is a set of tools and a table structure designed to make Frontier a powerful and easy-to-use CGI development environment. The two main parts of the CGI Framework are the suites.webServer table and the AppleEvent handler that processes events received from WebSTAR.
The webServer suite adds commands that make writing CGIs easier. The webServer.httpHeader command creates standard HTTP protocol headers that are needed by almost every CGI script. The webServer.errorMessage logs errors generated by CGI scripts and returns a user-defined error page. The webServer suite also defines tables to take advantage of WebSTAR’s custom action feature and to store macros that can be embedded in text objects or files.
The AppleEvent handler routes the parameters received to the appropriate script. It does this by creating a suffix mapping in WebStar that routes all requests ending in .fcgi to Frontier. Frontier uses the filename that was requested to determine which script or object to return to WebStar. Frontier ignores everything before the last / and chops off the .fcgi suffix to determine the script name. For example, if the requested URL was http://www.webedge.com/frontier/samples.tellTime.fcgi, Frontier would look for an object named samples.tellTime in the webServerScripts table.
The CGI Framework is restricted to serving only objects located in the webServerScripts table hierarchy. Not only scripts, but also string, word-processing text, and binary objects can be served from the webServerScripts table. Any macros embedded in string or text objects are automatically processed when these objects are served from the Object Database.
The AppleEvent handler wraps any CGI scripts in an error handler that traps any errors generated by the CGI. Any errors encountered during the CGI script execution are automatically logged and the error message is reported to the client.
With minor modification, AppleScript CGIs can be served from the Object Database, allowing existing AppleScript CGIs to take advantage of Frontier’s multi-threading capabilities and providing an easy migration path for webmasters who use AppleScript, but are interested in trying out Frontier. Step-by-step instructions are available on the Frontier CGI Scripting site.
The AppleEvent handler detects the scripting language used by the CGI script and formats the CGI parameters accordingly. In the case of UserTalk language CGIs, the parameters received from WebSTAR are parsed into a table and the address of the parameter table is passed as a single parameter to the CGI script.
A Sample CGI: The Pizza Processor
To illustrate how the CGI Framework works, I’ll use an example application that processes pizza orders taken from a web page. This example receives the form data, writes it to a tab-delimited text file and returns a confirmation message to the client. The form we are using has five fields: Name, Address, Phone, Size and Toppings. I’m not going the explain how to create forms using HTML, but the following line from the HTML coding is important to the running of this script:
<FORM ACTION="pizzaProcessor.fcgi" METHOD=POST>
This line tells the browser to send the encoded form data to pizzaProcessor.fcgi using the POST method. Based on the .fcgi extension, WebStar will send Frontier an Apple event containing the form data and other CGI-related information. The CGI Framework receives the Apple event and automatically decodes the form data. The field values are parsed into a sub-table named argTable in the parameter table along with other CGI values. The CGI Framework looks for a script named pizzaProcessor in the webServerScripts table. It runs the script, passing it the address of the parameter table as a parameter.
The POST method is important because the CGI Framework will only decode and parse the form data if it is sent using the POST method. It is possible to use the GET method, but you would have to decode the form data on your own.
Figure 4: Pizza Order Form
When the data gets to the pizzaProcessor script, the argTable should contain six values: Name, Address, Phone, Size, Toppings and Submit. All values will be strings except argTable.toppings which will be either a string or a list. Because it is a checkbox, multiple values can be selected. If more than one item is selected, argTable.toppings will be coerced into a list to accommodate the multiple values.
The UserTalk Code
The script we’ll use to process the information is named pizzaProcessor and is stored in the webServerScripts table. Here is a first look at the pizzaProcessor script:
pizzaProcessor : Version 1
on pizzaProcessor (params)
local
html = webServer.httpHeader ()
dataFile = file.getSystemDisk () + "Pizza Orders"
i
entry
on add (s)
html = html + s + cr
with params^
if defined (argTable)
entry = clock.now ()
« Create record
for i = 1 to sizeOf (argTable)
entry = entry + tab + string (argTable[i])
« Create data file
if not file.exists (dataFile)
file.new (dataFile)
file.setType (dataFile, 'TEXT')
« Write data to file
file.writeLine (dataFile, entry)
« Build return page
add ("<HTML><HEAD>")
add ("<TITLE>Thank You!</TITLE>")
add ("</HEAD><BODY>")
add ("<H1>Thank You!</H1>")
add ("</BODY></HTML>")
else
html = webServer.errorMessage ("No Data", params)
return (html)
Step By Step
Let me explain the code line by line.
on pizzaProcessor (params)
The first line defines the handler that is triggered when the script is run. It receives one parameter, params, which is the address of a table containing the CGI parameters sent from the web server.
local
html = webServer.httpHeader ()
dataFile = file.getSystemDisk () + "Pizza Orders"
i
entry
This is where we declare the local variables for the script. The first variable, html, is a string containing the page of HTML-formatted text to be returned to the client. Its initial value is a standard HTTP header created by the webServer.httpHeader command. The header is necessary for the browser to display the returned data properly.
The full path of the text file used to store the pizza order data is kept in the dataFile variable. It is set to use a file named Pizza Orders at the root level of the startup disk. A counter variable i is created for use in a loop later in the script. The entry variable will hold the individual records to be written to the data file.
on add (s)
html = html + s + cr
These two lines create a subroutine to ease adding text to the html variable. With this routine text can be appended to the html variable using add ("New Text") instead of html = html + "New Text".
with params^
This with statement allows the values in the parameter table to be called by name. Since params is a variable containing the address to the table of parameters, the ^ symbol is used to expand the variable to the contents of the table it points to.
if defined (argTable)
This line checks for the existence of a sub-table named argTable within the parameter table. The argTable is created automatically by the AppleEvent handler if form data is received using the POST method. If the argTable is defined, it is safe to assume data from the form was received.
entry = clock.now ()
« Create record
for i = 1 to sizeOf (argTable)
entry = entry + tab + string (argTable[i])
These lines create the record of the pizza order. The first line, entry = clock.now (), sets the entry variable to a string containing the current date and time. The last two lines loop through the argTable adding the value of each item and a tab separator to the record. Each item is coerced to a string to correctly deal with multiple selection items in the form, like checkboxes, which appear as a list in the argTable.
if not file.exists (dataFile)
file.new (dataFile)
file.setType (dataFile, 'TEXT')
These lines create a text file at the path specified by dataFile if one doesn’t already exist.
file.writeLine (dataFile, entry)
This line writes the record to the data file using the file.writeLine command. The file.writeLine command automatically opens and closes the file and adds a carriage return at the end of the line.
add ("<HTML><HEAD>")
add ("<TITLE>Thank You!</TITLE>")
add ("</HEAD><BODY>")
add ("<H1>Thank You!</H1>")
add ("</BODY></HTML>")
These five lines create the HTML page that will be returned to the client. They each use the add subroutine defined earlier to append text to the html variable. A more refined CGI would return more information, such as a confirmation with the total price of the order.
else
html = webServer.errorMessage ("No Data", params)
This else statement is part of the if defined (argTable) statement above. It is called if the argTable is not defined, meaning no form data was received by the CGI. The second line sets the html variable to the result of the webServer.errorMessage command. The webServer.errorMessage logs the error and returns a user-defined HTML-formatted error page. Using the webServer.errorMessage command to generate errors is encouraged, but not required. It logs error messages to a single location and creates a consistent look by using the same error page for multiple CGIs. The first parameter is the text of the error message, the second parameter is the address the parameter table. The webServer.errorMessage generates its own HTTP header, so the result completely overwrites the html variable rather than being appended to it.
return (html)
This last line returns the contents of the html variable, either a confirmation page or an error message, to WebStar, who returns it to the client. The return command ends script execution.
Making It Better
The pizzaProcessor script is a simple example of a common CGI application. However, it does have a few problems that need correction.
First, the order of the fields in the data file has not been defined. If a field is left blank, some browsers will return a nil value for the field, while other browsers will discard empty fields entirely. It is important to maintain a consistent field order in the data file so that it can be imported into a database.
Another issue that wasn’t addressed is defining fields that are required for processing. We can’t deliver a pizza if we don’t know at least the name, address and phone number of the customer and the size of pizza being ordered.
Lastly, since this script will be run in a multi-threaded environment, it is possible that two instances of the same script will attempt to write to the data file at precisely the same time. We need to use semaphores to lock the file so that only one script can write to it at a time.
An improved version of the script looks like this:
pizzaProcessor: Version 2
on pizzaProcessor (params)
local
html = webServer.httpHeader ()
dataFile = file.getSystemDisk () + "Pizza Orders"
i
entry
order = {"Name","Address", "Phone", "Size", "Toppings"}
order = {"Name","Address", "Phone", "Size", "Toppings"}
req = {"Name", "Address", "Phone", "Size"}
These two lines define two new variables. Order is a list of field names in the order they should appear in the data file. Req is a list of field names that must contain data for the order to be processed.
return (webserver.errorMessage (order[i] + " is required.", params))
entry = entry + tab
This is a new loop to create the record of the pizza order. Unlike the loop in the first version, this version loops through the items in the order list, checking that each value is defined and contains data before adding it to the record. If the field is empty the if req contains (order[i]) line checks for the field name in the list of required fields and returns an error if a required field is empty.
Semaphores
semaphores.lock (dataFile, 3600)
try
file.writeLine (dataFile, entry)
semaphores.unlock (dataFile)
else
semaphores.unlock (dataFile)
scriptError (tryError)
The above lines use the semaphores suite to lock and unlock the data file so that only one script will attempt to write to the file at a time. Using semaphores is very important when writing to a shared resource in multi-threaded situations.
Semaphores are like that little “occupied” sign on airplane restroom doors. When one person goes into the restroom they lock the door, which puts up the “occupied” sign. The next person comes to use the restroom, sees the sign and waits for first person to exit before opening the door.
If the first person forgets to lock the door, the “occupied” sign doesn’t light up and the next person is likely to walk in on him while he is in the restroom.
Semaphores are very similar. They manage which scripts have access to a shared resource at any given moment. Semaphores are needed when a script writes data to a shared resource, like a file or an object in the Object Database. Semaphores are not needed when reading data or when writing to local variables.
Frontier has a built-in semaphore suite to handle locking and unlocking access to shared data. The semaphores.lock command actually does two things. It waits for the resource if it is locked, and it locks the resource as soon as it becomes available.
Jumping back to the airplane restroom analogy, you wouldn’t seat someone in the restroom for the whole flight. Not just because it would be uncomfortable for the passenger but also because no one else would be able to use the restroom for the whole flight. The same thing applies to semaphores and scripts. A semaphore creates a bottleneck through which each script must pass one at a time. Don’t lock a resource for the entire running of the script. Lock the resource, write to it, then immediately unlock it.
It is always a good idea to put any semaphores.unlock commands in an try error handler. Like this:
semaphores.lock (filePath, 3600)
try
file.writeLine (filePath, data)
semaphores.unlock (filePath)
else
semaphores.unlock (filePath)
This way if an error occurs during the file.writeLine operation, the lock on the resource is still released. Restarting Frontier will reset any lingering locked semaphores.